#include "cpfsfuse.h"
//
#include <linux/types.h>
typedef int fmode_t;
#include <linux/fs.h>
//
#include <assert.h>
#include <errno.h>
#include <string.h>
//
#include <cxutil/logging.h>
#include <cxutil/utility.h>
//
#include "cpfs.h"

static int const known_fuse_open_flags =
    // O_LARGEFILE may be set to 0, but internally the kernel still uses
    // some marker value from asm-generic/fcntl.h
    (O_LARGEFILE ? O_LARGEFILE : 0100000)
    |O_RDONLY|O_WRONLY|O_RDWR // standard
    |O_NOFOLLOW // symlinks are done for us already
    |O_CREAT|O_EXCL|O_TRUNC // only passed to create
    |FMODE_EXEC // is this leaked by FUSE from exec?
    |O_NONBLOCK // this has no effect, all calls are blocking in cpfs
    ;
static int const uncertain_fuse_open_flags = 0100000;

static int cpfserrno_to_errno(CpfsErrno fserr)
{
    switch (fserr) {
    case NoError:           return 0;
    case BadFileDescriptor: return EBADF;
    case TooManyOpenFiles:  return EMFILE;
    case FilesystemFull:    return ENOSPC;
    case FilesystemCorrupt: return EIO;
    case FilenameTooLong:   return ENAMETOOLONG;
    case InvalidPath:       return EINVAL;
    case PathTooLong:       return ENAMETOOLONG;
    case PathNotFound:      return ENOENT;
    case DirectoryNotEmpty: return ENOTEMPTY;
    case IsRoot:            return EPERM;
    case FileExists:        return EEXIST;
    case IsDirectory:       return EISDIR;
    case NotDirectory:      return ENOTDIR;
    case NotSymlink:        return EINVAL;
    default:
        LOGF(Critical, "filesystem error code %d is unmapped", fserr);
        abort();
    }
}

#define RETURN_CPFS_ERROR_FUSE() \
    do { \
        assert(cpfs_errno_isset()); \
        return -cpfserrno_to_errno(cpfs_errno_get()); \
    } while (false)


static struct CpfsFuseData *get_cf_data()
{
    return fuse_get_context()->private_data;
}

static int cpfs_fuse_statfs(
        char const *OMIT_PARAM,
        struct statvfs *stfs)
{
    struct CpfsStatvfs cpfsstfs;
    if (cpfs_statvfs(get_cf_data()->cpfs, &cpfsstfs))
    {
        memset(stfs, 0, sizeof(*stfs));
        stfs->f_bsize = cpfsstfs.bsize;
        stfs->f_frsize = cpfsstfs.bsize;
        stfs->f_blocks = cpfsstfs.blocks;
        stfs->f_bfree = cpfsstfs.bfree;
        stfs->f_bavail = cpfsstfs.bfree;
        stfs->f_files = cpfsstfs.blocks - cpfsstfs.bfree;
        stfs->f_ffree = cpfsstfs.bfree;
        stfs->f_favail = cpfsstfs.bfree;
        // fsid?
        // flag?
        stfs->f_namemax = cpfsstfs.namemax;
        return 0;
    }
    else {
        RETURN_CPFS_ERROR_FUSE();
    }
}

static struct stat cpfsstat_to_stat(struct CpfsStat const *fsst)
{
    return (struct stat) {
        .st_ino = fsst->ino,
        .st_mode = (fsst->type << 12) | fsst->perms.perms,
        .st_nlink = fsst->nlink,
        .st_uid = fsst->uid,
        .st_gid = fsst->gid,
        .st_rdev = fsst->rdev,
        .st_size = fsst->size,
        .st_blksize = fsst->blksize,
        .st_atim = cpfstime_to_timespec(fsst->atime),
        .st_mtim = cpfstime_to_timespec(fsst->mtime),
        .st_ctim = cpfstime_to_timespec(fsst->ctime),
    };
}

static int cpfs_fuse_getattr(char const *path, struct stat *stbuf)
{
    struct CpfsStat cpfsstat;
    if (cpfs_stat(get_cf_data()->cpfs, path, &cpfsstat))
    {
        *stbuf = cpfsstat_to_stat(&cpfsstat);
        return 0;
    } else {
        RETURN_CPFS_ERROR_FUSE();
    }
}

static int cpfs_fuse_opendir(char const *path, struct fuse_file_info *info)
{
    int dirfd = cpfs_opendir(get_cf_data()->cpfs, path);
    if (dirfd == -1) {
        RETURN_CPFS_ERROR_FUSE();
    }
    else {
        info->fh = dirfd + 1;
        return 0;
    }
}

static int cpfs_fuse_open(
        char const *path,
        struct fuse_file_info *info)
{
    // creation flags imply create() should have been called
    assert(!(info->flags & (O_CREAT|O_EXCL|O_TRUNC)));

    int unknown = info->flags & ~known_fuse_open_flags;
    if (unknown) {
        unknown |= info->flags & uncertain_fuse_open_flags;
        LOGF(Warning, "Unknown flags 0%o (path=\"%s\")", unknown, path);
    }
    int fd = cpfs_open(get_cf_data()->cpfs, path);
    if (fd != -1) {
        info->fh = fd + 1;
        return 0;
    } else {
        RETURN_CPFS_ERROR_FUSE();
    }
}

static int cpfs_fuse_create(
        char const *const path,
        mode_t const mode,
        struct fuse_file_info *const info)
{
    // why can i assume O_WRONLY here?!
    assert(info->flags & (O_CREAT|O_WRONLY|O_TRUNC));

    int unknown = info->flags & ~known_fuse_open_flags;
    if (unknown) {
        unknown |= info->flags & uncertain_fuse_open_flags;
        LOGF(Warning, "Unknown flags 0%o (path=\"%s\")", unknown, path);
    }
    struct fuse_context const *const fusectx = fuse_get_context();
    int const fd = cpfs_create(
        get_cf_data()->cpfs,
        path,
        mode >> 12,
        (CpfsPerms){.perms = mode & 07777},
        fusectx->uid,
        fusectx->gid);
    if (fd != -1) {
        info->fh = fd + 1;
        return 0;
    } else {
        RETURN_CPFS_ERROR_FUSE();
    }
}

// note: to have ino autofilled, fuse expects -1
static int cpfs_fuse_readdir(
    const char *OMIT_PARAM,
    void *const fusedata,
    fuse_fill_dir_t const filler,
    off_t offset,
    struct fuse_file_info *const fileinfo)
{
    struct CpfsFuseData const *const cfdata = get_cf_data();
    char name[cpfs_params(cfdata->cpfs)->namemax + 1];
    memset(name, 0, sizeof(name));
    struct CpfsStat cpfsstat;
    if (!cpfs_readdir(cfdata->cpfs, fileinfo->fh - 1, &offset, name, &cpfsstat, false)) {
        RETURN_CPFS_ERROR_FUSE();
    } else if (!offset) {
        // end of directory
        return 0;
    } else {
        struct stat st = cpfsstat_to_stat(&cpfsstat);
        verify(!filler(fusedata, name, &st, offset));
        return 0;
    }
}

static int cpfs_fuse_releasedir(
        char const *OMIT_PARAM,
        struct fuse_file_info *info)
{
    assert(info->fh > 0);
    if (!cpfs_releasedir(get_cf_data()->cpfs, info->fh - 1)) {
        RETURN_CPFS_ERROR_FUSE();
    }
    else {
        return 0;
    }
}

// fuse ignores the return value of release()
static int cpfs_fuse_release(
        char const *OMIT_PARAM,
        struct fuse_file_info *fi)
{
    assert(fi->fh);
    if (cpfs_close(get_cf_data()->cpfs, fi->fh - 1))
        return 0;
    else
        RETURN_CPFS_ERROR_FUSE();
}

static int cpfs_fuse_mknod(
        const char *const path,
        mode_t const mode,
        dev_t const dev)
{
    if (dev) {
        LOG(Error, "Filesystem does not support device IDs");
        return -EPERM; // mknod(2) on Linux
    }
    struct fuse_context const *const fusectx = fuse_get_context();
    if (cpfs_mknod(
            get_cf_data()->cpfs,
            path,
            mode >> 12,
            (CpfsPerms){.perms = mode & 07777},
            fusectx->uid,
            fusectx->gid))
        return 0;
    else
        RETURN_CPFS_ERROR_FUSE();
}

static int cpfs_fuse_utimens(
        const char *path,
        const struct timespec tv[2])
{
    CpfsTime const ct[2] = {
        timespec_to_cpfstime(&tv[0]),
        timespec_to_cpfstime(&tv[1]),
    };
    if (cpfs_utimens(get_cf_data()->cpfs, path, ct)) {
        return 0;
    } else {
        RETURN_CPFS_ERROR_FUSE();
    }
}

static int cpfs_fuse_write(
        const char *OMIT_PARAM,
        const char *buf,
        size_t size,
        off_t off,
        struct fuse_file_info *fi)
{
    int writeret = cpfs_write(get_cf_data()->cpfs, fi->fh - 1, buf, size, off);
    if (writeret != -1)
        return writeret;
    else
        RETURN_CPFS_ERROR_FUSE();
}

static int cpfs_fuse_read(
        const char *OMIT_PARAM,
        char *buf,
        size_t size,
        off_t off,
        struct fuse_file_info *fi)
{
    int readret = cpfs_read(get_cf_data()->cpfs, fi->fh - 1, buf, size, off);
    if (readret != -1)
        return readret;
    else
        RETURN_CPFS_ERROR_FUSE();
}

static int cpfs_fuse_truncate(
        const char *path,
        off_t off)
{
    if (cpfs_truncate(get_cf_data()->cpfs, path, off)) {
        return 0;
    }
    else {
        RETURN_CPFS_ERROR_FUSE();
    }
}

static int cpfs_fuse_flush(
        const char *OMIT_PARAM, // path
        struct fuse_file_info *fi)
{
    if (cpfs_fnop(get_cf_data()->cpfs, fi->fh - 1))
        return 0;
    else
        RETURN_CPFS_ERROR_FUSE();
}

static int cpfs_fuse_sync(
        char const *OMIT_PARAM, // path
        int OMIT_PARAM, // datasync
        struct fuse_file_info *fi)
{
    if (cpfs_fnop(get_cf_data()->cpfs, fi->fh - 1)) {
        return 0;
    } else {
        RETURN_CPFS_ERROR_FUSE();
    }
}

static int cpfs_fuse_mkdir(
        const char *path,
        mode_t mode)
{
    if (mode & ~(S_IFDIR|07777)) {
        LOGF(Warning, "Unexpected bits in mode %o", mode);
    }
    mode |= S_IFDIR;
    struct fuse_context const *fusectx = fuse_get_context();
    if (cpfs_mknod(
            get_cf_data()->cpfs,
            path,
            mode >> 12,
            (CpfsPerms){.perms = mode & 07777},
            fusectx->uid,
            fusectx->gid)) {
        return 0;
    }
    else {
        RETURN_CPFS_ERROR_FUSE();
    }
}

static int cpfs_fuse_rmdir(
        const char *path)
{
    if (cpfs_rmdir(get_cf_data()->cpfs, path)) {
        return 0;
    } else {
        RETURN_CPFS_ERROR_FUSE();
    }
}

static int cpfs_fuse_unlink(
        const char *path)
{
    if (cpfs_unlink(get_cf_data()->cpfs, path)) {
        return 0;
    } else {
        RETURN_CPFS_ERROR_FUSE();
    }
}

static int cpfs_fuse_chmod(
        char const *path,
        mode_t mode)
{
    if (cpfs_chmod(get_cf_data()->cpfs, path, (CpfsPerms){.perms = mode & 07777})) {
        return 0;
    } else {
        RETURN_CPFS_ERROR_FUSE();
    }
}

static int cpfs_fuse_rename(
        char const *oldpath,
        char const *newpath)
{
    if (cpfs_rename(get_cf_data()->cpfs, oldpath, newpath))
        return 0;
    else
        RETURN_CPFS_ERROR_FUSE();
}

static int cpfs_fuse_chown(
        const char *path,
        uid_t uid,
        gid_t gid)
{
    if (cpfs_chown(get_cf_data()->cpfs, path, uid, gid))
        return 0;
    else
        RETURN_CPFS_ERROR_FUSE();
}

static int cpfs_fuse_symlink(
        const char *target,
        const char *path)
{
    struct fuse_context const *fusectx = fuse_get_context();
    if (cpfs_symlink(
            get_cf_data()->cpfs,
            target, path,
            fusectx->uid, fusectx->gid))
        return 0;
    else
        RETURN_CPFS_ERROR_FUSE();
}

static int cpfs_fuse_readlink(
        const char *path,
        char *buf,
        size_t len)
{
    if (cpfs_readlink(get_cf_data()->cpfs, path, buf, len))
        return 0;
    else
        RETURN_CPFS_ERROR_FUSE();
}

static int cpfs_fuse_link(
        char const *oldpath,
        char const *newpath)
{
    if (cpfs_link(get_cf_data()->cpfs, oldpath, newpath))
        return 0;
    else
        RETURN_CPFS_ERROR_FUSE();
}

static int cpfs_fuse_ftruncate(
        char const *const OMIT_PARAM,
        off_t length,
        struct fuse_file_info *const fileinfo)
{
    if (cpfs_ftruncate(get_cf_data()->cpfs, fileinfo->fh - 1, length))
        return 0;
    else
        RETURN_CPFS_ERROR_FUSE();
}

static int cpfs_fuse_fgetattr(
        char const *OMIT_PARAM,
        struct stat *const st,
        struct fuse_file_info *const fi)
{
    struct CpfsStat cst = {-1}; // TODO memset -1
    if (cpfs_fstat(get_cf_data()->cpfs, fi->fh - 1, &cst)) {
        *st = cpfsstat_to_stat(&cst);
        return 0;
    } else {
        RETURN_CPFS_ERROR_FUSE();
    }
}

void *cpfs_fuse_init(struct fuse_conn_info *conn VARATTR_UNUSED)
{
    (void)conn;
    LOGF(Debug, "Private data is %p", get_cf_data());
    return get_cf_data();
}

void cpfs_fuse_destroy(void *data)
{
    LOG(Debug, NULL);
    assert(data == get_cf_data());
}

struct fuse_operations cpfs_oper =
{
    .getattr = cpfs_fuse_getattr,
    .readlink = cpfs_fuse_readlink,
    .getdir = NULL, // deprecated by readdir
    .mknod = cpfs_fuse_mknod,
    .mkdir = cpfs_fuse_mkdir,
    .unlink = cpfs_fuse_unlink,
    .rmdir = cpfs_fuse_rmdir,
    .symlink = cpfs_fuse_symlink,
    .rename = cpfs_fuse_rename,
    .link = cpfs_fuse_link,
    .chmod = cpfs_fuse_chmod,
    .chown = cpfs_fuse_chown,
    .truncate = cpfs_fuse_truncate,
    .utime = NULL, // deprecated by utimens
    .open = cpfs_fuse_open,
    .read = cpfs_fuse_read,
    .write = cpfs_fuse_write,
    .statfs = cpfs_fuse_statfs,
    .flush = cpfs_fuse_flush,
    .release = cpfs_fuse_release,
    .fsync = cpfs_fuse_sync,
    .setxattr = NULL,
    .getxattr = NULL,
    .listxattr = NULL,
    .removexattr = NULL,
    .opendir = cpfs_fuse_opendir,
    .readdir = cpfs_fuse_readdir,
    .releasedir = cpfs_fuse_releasedir,
    .fsyncdir = cpfs_fuse_sync,
    .init = cpfs_fuse_init, // anything required here?
    .destroy = cpfs_fuse_destroy, // anything required here?
    .access = NULL, // this is handled by -o default_permissions
    .create = cpfs_fuse_create,
    .ftruncate = cpfs_fuse_ftruncate,
    .fgetattr = cpfs_fuse_fgetattr,
    .lock = NULL,
    .utimens = cpfs_fuse_utimens,
    .bmap = NULL,
    .flag_nullpath_ok = 1,
    // 2.8
    .ioctl = NULL,
    .poll = NULL,
};
